/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package phd.dupenois.changeidentifiers;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import phd.dupenois.changeidentifiers.exceptions.ChangeIdentifierLoadException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Properties;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import phd.dupenois.changeidentifiers.dotpatterns.DotPattern;
import phd.dupenois.changeidentifiers.exceptions.ChangeIdentifierSetNotInitialisedException;
import phd.dupenois.changeidentifiers.exceptions.ChangeIdentifierSetNotValidException;
import phd.dupenois.changeidentifiers.footprints.Footprint;

/**
 *
 * @author Max
 */
public class ChangeIdentifierSet{

    private SAXParser parser;
    private SAXParserFactory factory;
    private ChangeIdentifierXMLHandler handler;
    private ChangeIdentifierHolder[] changeIdentifiers;
    private ChangeIdentifierListener listener;
    private boolean continueCheckingForChanges;
    private boolean initialised;
    private boolean concurrent;

    private int maxFails;
    private int failCount;
    private double totalThreshold;
    private double totalChange;
    private int changeIdentifierCount;
    private int currentTimeStep;

    private String version;
    private String name;
    private String description;

    private boolean redraw;
    private ChangeIdentifierSetSnapshot snapshot;

    private ExecutorService threadPool;

    /**
     * @return the concurrent
     */
    public boolean isConcurrent() {
        return concurrent;
    }

    /**
     * @param concurrent the concurrent to set
     */
    public void setConcurrent(boolean concurrent) {
        this.concurrent = concurrent;
    }

    /**
     * @return the initialised
     */
    public boolean isInitialised() {
        return initialised;
    }

    private static class ChangeIdentifierXMLHandler extends DefaultHandler{
        private ChangeIdentifierSet set;
        private ChangeIdentifierHolder currentHolder;
        //private String currentElement;
        private Vector<String> elementList;
        private Properties ci_properties;
        private Properties set_properties;
        private boolean inChangeIdentifier;
        public ChangeIdentifierXMLHandler(ChangeIdentifierSet set){
            super();
            this.set = set;
            elementList = new Vector<String>();
            ci_properties = new Properties();
            set_properties = new Properties();
            inChangeIdentifier = false;
        }

        @Override
        public void startElement(String uri, String localName, String qualifiedName, Attributes attributes) throws SAXException {
            //qualified name is element name
            String normalised = qualifiedName.toUpperCase();
            elementList.add(normalised);
            if(normalised.equals("CHANGEIDENTIFIER")){
                //nothing
                ci_properties = new Properties();
                inChangeIdentifier = true;
                setAttributes(ci_properties, attributes);
            }else if(normalised.equals("CHANGEIDENTIFIERSET")){
                setAttributes(set_properties, attributes);
            }
        }
        private void setAttributes(Properties properties, Attributes attributes){
            String attName;
            for(int i =0, max = attributes.getLength(); i< max; i++){
                attName = attributes.getQName(i);
                properties.setProperty(attName.toUpperCase(), attributes.getValue(attName));
            }
        }
        @Override
        public void characters(char[] charArray, int start, int length) throws SAXException {
            StringBuffer data = new StringBuffer();
            for(int i=start;i<start+length;i++) data.append(charArray[i]);
            String dataString = data.toString().trim();
            if(dataString.equals(""))return;
            if(inChangeIdentifier){
                ci_properties.setProperty(elementList.lastElement(), dataString);
            }else{
                set_properties.setProperty(elementList.lastElement(), dataString);
            }
        }


        @Override
        public void endElement(String uri, String localName, String qualifiedName) throws SAXException {
            //qualified name is element name
            String normalised = qualifiedName.toUpperCase();
            elementList.remove(elementList.size()-1);
            if(normalised.equals("CHANGEIDENTIFIER")){
                String location = ci_properties.getProperty("LOCATION");
                String classname = ci_properties.getProperty("CLASSNAME");
                int priority;
                try {
                    priority = Integer.parseInt(ci_properties.getProperty("PRIORITY"));
                } catch (NumberFormatException numberFormatException) {
                    priority= 0;
                }
                double threshold;
                try {
                    threshold = Double.parseDouble(ci_properties.getProperty("THRESHOLD"));
                } catch (NumberFormatException numberFormatException) {
                    threshold= 1;
                }
                double multiplier;
                try {
                    multiplier = Double.parseDouble(ci_properties.getProperty("MULTIPLIER"));
                } catch (NumberFormatException numberFormatException) {
                    multiplier= 1;
                }
                boolean redrawOnFail = Boolean.parseBoolean(ci_properties.getProperty("REDRAWONFAIL"));
                try {
                    set.add(new ChangeIdentifierHolder(location, classname, priority, threshold, redrawOnFail, multiplier));
                } catch (ChangeIdentifierLoadException ex) {
                    throw new SAXException((Exception)ex.getCause());
                }
                inChangeIdentifier = false;
            }else if(normalised.equals("CHANGEIDENTIFIERSET")){
                double threshold;
                try {
                    threshold = Double.parseDouble(set_properties.getProperty("THRESHOLD"));
                } catch (NumberFormatException numberFormatException) {
                    threshold= 1;
                }
                int maxFails;
                try {
                    maxFails = Integer.parseInt(set_properties.getProperty("MAXFAILS"));
                } catch (NumberFormatException numberFormatException) {
                    maxFails= 1;
                }
                set.setMaxFails(maxFails);
                set.setTotalThreshold(threshold);
                set.setDescription(set_properties.getProperty("DESCRIPTION").trim());
                set.setName(set_properties.getProperty("NAME"));
                set.setVersion(set_properties.getProperty("VER"));
                set.setConcurrent(Boolean.parseBoolean(set_properties.getProperty("CONCURRENT")));
            }

        }
    }


    public ChangeIdentifierSet(){
        initialised = false;
        this.currentTimeStep = 0;
    }
    public void resetTimeStepCount(){
        this.currentTimeStep = 0;
    }
//    public void setPropertiesHolder(PropertiesHolder propertiesHolder){
//        for(ChangeIdentifierHolder holder :changeIdentifiers){
//            holder.getInstance().setPropertiesHolder(propertiesHolder);
//        }
//    }
    public void add(ChangeIdentifierHolder identifier){
        if(changeIdentifiers==null) {
            changeIdentifiers = new ChangeIdentifierHolder[1];
            changeIdentifiers[0] = identifier;
        }else{
            Vector<ChangeIdentifierHolder> dummy = new Vector<ChangeIdentifierHolder>();
//            ChangeIdentifierHolder[] dummy = new ChangeIdentifierHolder[changeIdentifiers.length+1];
            for(int i=0; i<changeIdentifiers.length; i++)dummy.add(changeIdentifiers[i]);
            dummy.add(identifier);
            Collections.sort(dummy);
            changeIdentifiers = dummy.toArray(new ChangeIdentifierHolder[dummy.size()]);
        }
    }

    public void loadFromXML(String location) throws ParserConfigurationException, SAXException, IOException{
        if(factory==null){
            factory = SAXParserFactory.newInstance();
            factory.setValidating(false);
            parser = factory.newSAXParser();
        }
        handler = new ChangeIdentifierXMLHandler(this);
        parser.parse(location, handler);
    }

    public void initialise(ChangeIdentifierListener listener) throws ChangeIdentifierSetNotValidException {
        this.listener = listener;
        initialised = true;
        validate();

        threadPool = Executors.newFixedThreadPool(this.changeIdentifiers.length*5);
     
    }

    public void validate() throws ChangeIdentifierSetNotValidException{
        if(getName()==null||getName().length()<1)
            throw new ChangeIdentifierSetNotValidException("No name given.");
        if(getVersion()==null||getVersion().length()<1)
            throw new ChangeIdentifierSetNotValidException("No version given.");
        if(getDescription()==null||getDescription().length()<1)
            throw new ChangeIdentifierSetNotValidException("No description give" +
                    "n.");
        if(getChangeIdentifierHolders().length<1)
            throw new ChangeIdentifierSetNotValidException("No change identifiers in the set.");
    }
    private HashMap<ChangeIdentifierHolder, Long> startTimeMap;
    public boolean redraw(Footprint previousFootprint, DotPattern previousDotPattern, DotPattern currentDotPattern)
        throws ChangeIdentifierSetNotInitialisedException
    {
        if(!isInitialised()) throw new ChangeIdentifierSetNotInitialisedException("Change Identifier Set has not been initialised");
        this.currentTimeStep++;
        this.totalChange = 0;
        this.redraw = false;
        this.continueCheckingForChanges = (changeIdentifiers.length>0);
        this.changeIdentifierCount = changeIdentifiers.length;
        long starttime = System.currentTimeMillis();
        this.listener.startedChangeIdentifierSet(starttime);
        this.snapshot = new ChangeIdentifierSetSnapshot(starttime);
        if(isConcurrent()){
            startTimeMap = new HashMap<ChangeIdentifierHolder, Long>();
            Runnable runnable;
            for(ChangeIdentifierHolder holder : changeIdentifiers){
                holder.setTimeStep(this.currentTimeStep);
                if(continueCheckingForChanges()){
                    try {
                        runnable = new ChangeIdentifierThread(holder, previousFootprint,
                                previousDotPattern, currentDotPattern);
                        startTimeMap.put(holder, System.currentTimeMillis());
                        this.threadPool.execute(runnable);
                    } catch (RejectedExecutionException e) {
                        Throwable cause = e.getCause();
                        System.out.println("Rejected "+e.getMessage());
                        System.out.println("Cause: "+(cause == null ? null : cause.toString()));
                    }
                }
            }
//            System.out.println("--Time taken to start cis: "+(System.currentTimeMillis()-starttime)+"ms");
            //Wait till we're done
//            long startofWait = System.currentTimeMillis();
            while(continueCheckingForChanges()){
                try {
                    Thread.sleep(0, 1000);
                } catch (InterruptedException interruptedException) {
                }
            }
//            System.out.println("--Wait time for cis: "+(System.currentTimeMillis()-startofWait)+"ms");
        }else{

//            Vector<ChangeIdentifierHolder> ciVector = new Vector<ChangeIdentifierHolder>();
////            ChangeIdentifierHolder[] dummy = new ChangeIdentifierHolder[changeIdentifiers.length+1];
//            for(int i=0; i<changeIdentifiers.length; i++)ciVector.add(changeIdentifiers[i]);
//            Iterator<ChangeIdentifierHolder> iter = ciVector.iterator();
            ChangeIdentifierHolder holder;
            for(int i=0, length = changeIdentifiers.length; i < length &&
                    continueCheckingForChanges();i++){
                holder = changeIdentifiers[i];
                runChangeIdentifier(holder, previousFootprint, previousDotPattern, currentDotPattern);
            }
        }
        long timeTaken = System.currentTimeMillis()-starttime;
//        if(timeTaken>5)System.out.println("--Apparent time taken: "+timeTaken+"ms");
        this.snapshot.setFailcount(this.failCount);
        this.snapshot.setRedraw(this.redraw);
        this.snapshot.setValue(this.totalChange);
        this.snapshot.setTimeTaken(timeTaken);
        this.listener.finishedChangeIdentifierSet(timeTaken);
//        this.threadPool.shutdownNow();
        return this.redraw;
    }

    public void changeIdentifierFinished(ChangeIdentifierHolder holder, double value, long starttime, long timeTaken){
        if(!continueCheckingForChanges()) return;
        this.changeIdentifierCount--;
        if(this.changeIdentifierCount<1){
            stopCheckingForChanges();
        }
        ChangeIdentifier instance = holder.getInstance();

        boolean fail = (value>holder.getThreshold());
        boolean causedRedraw = false;
        if(fail){
            listener.changeIdentifierBrokenOwnThreshold(instance, holder.getThreshold(), value, holder.isRedrawOnFail());

            this.failCount++;
            if(failCount > maxFails){
                causedRedraw = true;
                listener.changeIdentifierBrokenMaxFail(instance, maxFails);
                stopCheckingForChanges();
                this.redraw = true;
            }
            if(holder.isRedrawOnFail()){
                causedRedraw = true;
                stopCheckingForChanges();
                this.redraw = true;
            }
        }

        this.totalChange += value*holder.getMultiplier();
        if(this.totalChange > this.totalThreshold){
            causedRedraw = true;
             listener.changeIdentifierBrokenTotalThreshold(instance, this.totalThreshold,
                     value*holder.getMultiplier(), this.totalChange);
            stopCheckingForChanges();
            this.redraw = true;
        }
        if(continueCheckingForChanges()||causedRedraw){
            this.snapshot.addChangeIdentifier(instance.getClass().getCanonicalName(),
                    value, fail, causedRedraw, starttime, timeTaken);
        }
    }
    private void stopCheckingForChanges(){
        this.continueCheckingForChanges = false;
        
    }
    private boolean continueCheckingForChanges(){
        return this.continueCheckingForChanges;
    }
    public ChangeIdentifierHolder[] getChangeIdentifierHolders(){
        return this.changeIdentifiers;
    }
    
    private class ChangeIdentifierThread implements Runnable{
        private ChangeIdentifierHolder holder;
        private Footprint previousFootprint;
        private DotPattern previousDotPattern;
        private DotPattern currentDotPattern;
        public ChangeIdentifierThread(ChangeIdentifierHolder holder,
                Footprint previousFootprint, DotPattern previousDotPattern,
                DotPattern currentDotPattern){
            this.holder = holder;
            this.previousFootprint = previousFootprint;
            this.previousDotPattern = previousDotPattern;
            this.currentDotPattern = currentDotPattern;
        }

        public void run() {
//            System.out.println("--Time taken to start thread: "+(System.currentTimeMillis()-startTimeMap.get(this.holder))+"ms");
            try{
                if(Thread.interrupted())throw new InterruptedException();
                if(!continueCheckingForChanges())return;
                runChangeIdentifier(holder, previousFootprint, previousDotPattern,
                                    currentDotPattern);
            }catch(InterruptedException ex){
                return;
            }
        }
    }
    private void runChangeIdentifier(ChangeIdentifierHolder holder,
                Footprint previousFootprint, DotPattern previousDotPattern,
                DotPattern currentDotPattern){
        ChangeIdentifier instance = holder.getInstance();
        long starttime = System.currentTimeMillis();
        listener.startedChangeIdentifier(instance, starttime);
        double change = 0;
        try{

           change = instance.calculateChange(previousFootprint, previousDotPattern, currentDotPattern);
        }catch(RuntimeException ex){
            listener.exceptionThrown(ex);
        }catch(Exception ex){
            listener.exceptionThrown(ex);
        }
        long timeTaken = System.currentTimeMillis()-starttime;
//        System.out.println("--Actual time taken for identifier: "+timeTaken+"ms");
        listener.finishedChangeIdentifier(instance, timeTaken);
        if(!continueCheckingForChanges())return;

//        long notifyofFinishTime = System.currentTimeMillis();
        changeIdentifierFinished(holder, change, starttime, timeTaken);

//        System.out.println("--Time taken for change identifier finish: "+(System.currentTimeMillis()-notifyofFinishTime)+"ms");
    }

    /**
     * @return the maxFails
     */
    public int getMaxFails() {
        return maxFails;
    }

    /**
     * @param maxFails the maxFails to set
     */
    public void setMaxFails(int maxFails) {
        this.maxFails = maxFails;
    }

    /**
     * @return the totalThreshold
     */
    public double getTotalThreshold() {
        return totalThreshold;
    }

    /**
     * @param totalThreshold the totalThreshold to set
     */
    public void setTotalThreshold(double totalThreshold) {
        this.totalThreshold = totalThreshold;
    }

    /**
     * @return the snapshot
     */
    public ChangeIdentifierSetSnapshot getSnapshot() {
        return snapshot;
    }

    /**
     * @return the version
     */
    public String getVersion() {
        return version;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param version the version to set
     */
    public void setVersion(String version) {
        this.version = version;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    public void saveToFile(File file)throws IOException{
        BufferedWriter writer = new BufferedWriter(new FileWriter(file));
        writer.write(toXMLString());
        writer.close();
    }

    public String toXMLString(){
        StringBuffer xml = new StringBuffer();
        xml.append("<changeidentifierset name=\""+getName()+"\" ver=\""+getVersion()+"\">\n");
        xml.append("\t<description>");
        xml.append(getDescription());
        xml.append("</description>\n");
        xml.append("\t<threshold>");
        xml.append(getTotalThreshold()+"");
        xml.append("</threshold>\n");
        xml.append("\t<maxFails>");
        xml.append(getMaxFails()+"");
        xml.append("</maxFails>\n");
        xml.append("\t<concurrent>");
        xml.append(isConcurrent()+"");
        xml.append("</concurrent>\n");
        for(ChangeIdentifierHolder holder: changeIdentifiers){
            xml.append("\t<changeidentifier>\n");
            xml.append("\t\t<location>");
            xml.append(holder.getLocation());
            xml.append("</location>\n");
            xml.append("\t\t<classname>");
            xml.append(holder.getClassname());
            xml.append("</classname>\n");
            xml.append("\t\t<priority>");
            xml.append(holder.getPriority()+"");
            xml.append("</priority>\n");
            xml.append("\t\t<threshold>");
            xml.append(holder.getThreshold()+"");
            xml.append("</threshold>\n");
            xml.append("\t\t<multiplier>");
            xml.append(holder.getMultiplier()+"");
            xml.append("</multiplier>\n");
            xml.append("\t\t<redrawOnFail>");
            xml.append(holder.isRedrawOnFail()+"");
            xml.append("</redrawOnFail>\n");
            xml.append("\t</changeidentifier>\n");
        }

        xml.append("</changeidentifierset>");
        return xml.toString();
    }

}
